#include "api.h"
#include "math/math.h"
#include "core/maf.h"
#include "util.h"
#ifdef LOVR_USE_LUAU
#include <luacode.h>
#endif
#include "lib/luax/lutf8lib.h"
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#ifndef LOVR_DISABLE_GRAPHICS
#include "data/modelData.h"
#include "graphics/graphics.h"
#endif

typedef void voidFn(void);
typedef void destructorFn(void*);

#ifdef _WIN32
#define LOVR_EXPORT __declspec(dllexport)
#else
#define LOVR_EXPORT __attribute__((visibility("default")))
#endif

LOVR_EXPORT int luaopen_lovr(lua_State* L);
LOVR_EXPORT int luaopen_lovr_audio(lua_State* L);
LOVR_EXPORT int luaopen_lovr_data(lua_State* L);
LOVR_EXPORT int luaopen_lovr_event(lua_State* L);
LOVR_EXPORT int luaopen_lovr_filesystem(lua_State* L);
LOVR_EXPORT int luaopen_lovr_graphics(lua_State* L);
LOVR_EXPORT int luaopen_lovr_headset(lua_State* L);
LOVR_EXPORT int luaopen_lovr_math(lua_State* L);
LOVR_EXPORT int luaopen_lovr_physics(lua_State* L);
LOVR_EXPORT int luaopen_lovr_system(lua_State* L);
LOVR_EXPORT int luaopen_lovr_thread(lua_State* L);
LOVR_EXPORT int luaopen_lovr_timer(lua_State* L);

// Object names are lightuserdata because Variants need a non-Lua string due to threads.
static int luax_meta__tostring(lua_State* L) {
  lua_getfield(L, 1, "__info");
  TypeInfo* info = lua_touserdata(L, -1);
  lua_pop(L, 1);
  Proxy* p = lua_touserdata(L, 1);
  lua_pushfstring(L, "%s: %p", info->name, p->object);
  return 1;
}

// Currently the same as tostring
static int luax_type(lua_State* L) {
  lua_getfield(L, 1, "__info");
  TypeInfo* info = lua_touserdata(L, -1);
  lua_pushstring(L, info->name);
  return 1;
}

static int luax_meta__gc(lua_State* L) {
  Proxy* p = lua_touserdata(L, 1);
  if (p) {
    // Remove from userdata cache
    lua_getfield(L, LUA_REGISTRYINDEX, "_lovrobjects");
    if (lua_istable(L, -1)) {
      lua_pushlightuserdata(L, p->object);
      lua_pushnil(L);
      lua_rawset(L, -3);
    }
    lua_pop(L, 1);

    // Release
    lua_getmetatable(L, 1);
    lua_getfield(L, -1, "__info");
    TypeInfo* info = lua_touserdata(L, -1);
    if (info->destructor) {
      lovrRelease(p->object, info->destructor);
      p->object = NULL;
    }
  }
  return 0;
}

static int luax_runfinalizers(lua_State* L) {
  lua_getfield(L, LUA_REGISTRYINDEX, "_lovrfinalizers");
  for (int i = luax_len(L, 2); i >= 1; i--) {
    lua_rawgeti(L, 2, i);
    voidFn* finalizer = (voidFn*) lua_tocfunction(L, -1);
    finalizer();
    lua_pop(L, 1);
  }
  return 0;
}

void luax_preload(lua_State* L) {
  static const luaL_Reg lovrModules[] = {
    { "lovr", luaopen_lovr },
#ifndef LOVR_DISABLE_AUDIO
    { "lovr.audio", luaopen_lovr_audio },
#endif
#ifndef LOVR_DISABLE_DATA
    { "lovr.data", luaopen_lovr_data },
#endif
#ifndef LOVR_DISABLE_EVENT
    { "lovr.event", luaopen_lovr_event },
#endif
#ifndef LOVR_DISABLE_FILESYSTEM
    { "lovr.filesystem", luaopen_lovr_filesystem },
#endif
#ifndef LOVR_DISABLE_GRAPHICS
    { "lovr.graphics", luaopen_lovr_graphics },
#endif
#ifndef LOVR_DISABLE_HEADSET
    { "lovr.headset", luaopen_lovr_headset },
#endif
#ifndef LOVR_DISABLE_MATH
    { "lovr.math", luaopen_lovr_math },
#endif
#ifndef LOVR_DISABLE_PHYSICS
    { "lovr.physics", luaopen_lovr_physics },
#endif
#ifndef LOVR_DISABLE_SYSTEM
    { "lovr.system", luaopen_lovr_system },
#endif
#ifndef LOVR_DISABLE_THREAD
    { "lovr.thread", luaopen_lovr_thread },
#endif
#ifndef LOVR_DISABLE_TIMER
    { "lovr.timer", luaopen_lovr_timer },
#endif
#ifndef LOVR_DISABLE_UTF8
    { "utf8", luaopen_utf8 },
#endif
    { NULL, NULL }
  };

  lua_getglobal(L, "package");
  lua_getfield(L, -1, "preload");
  luax_register(L, lovrModules);
  lua_pop(L, 2);
}

void _luax_registertype(lua_State* L, const char* name, const luaL_Reg* functions, destructorFn* destructor) {

  // Push metatable
  luaL_newmetatable(L, name);
  lua_getmetatable(L, -1);

  // m.__index = m
  lua_pushvalue(L, -1);
  lua_setfield(L, -1, "__index");

  // m.__info = info
  TypeInfo* info = lua_newuserdata(L, sizeof(TypeInfo));
  info->name = name;
  info->destructor = destructor;
  lua_setfield(L, -2, "__info");

  // m.__gc = gc
  lua_pushcfunction(L, luax_meta__gc);
  lua_setfield(L, -2, "__gc");

  // m.__close = gc
  lua_pushcfunction(L, luax_meta__gc);
  lua_setfield(L, -2, "__close");

  // m.__tostring
  lua_pushcfunction(L, luax_meta__tostring);
  lua_setfield(L, -2, "__tostring");

  // Register class methods
  if (functions) {
    luax_register(L, functions);
  }

  // :release method
  lua_pushcfunction(L, luax_meta__gc);
  lua_setfield(L, -2, "release");

  // :type method
  lua_pushcfunction(L, luax_type);
  lua_setfield(L, -2, "type");

  // Pop metatable
  lua_pop(L, 1);
}

void* _luax_totype(lua_State* L, int index, uint64_t hash) {
  Proxy* p = lua_touserdata(L, index);

  if (p && lua_type(L, index) != LUA_TLIGHTUSERDATA && p->hash == hash) {
    return p->object;
  }

  return NULL;
}

void* _luax_checktype(lua_State* L, int index, uint64_t hash, const char* debug) {
  void* object = _luax_totype(L, index, hash);

  if (!object) {
    luax_typeerror(L, index, debug);
  }

  return object;
}

int luax_typeerror(lua_State* L, int index, const char* expected) {
  const char* name;
  if (luaL_getmetafield(L, index, "__name") == LUA_TSTRING) {
    name = lua_tostring(L, -1);
  } else if (lua_type(L, index) == LUA_TLIGHTUSERDATA) {
    name = "light userdata";
  } else {
    name = luaL_typename(L, index);
  }
  const char* message = lua_pushfstring(L, "%s expected, got %s", expected, name);
  return luaL_argerror(L, index, message);
}

// Registers the userdata on the top of the stack in the registry.
void _luax_pushtype(lua_State* L, const char* type, uint64_t hash, void* object) {
  if (!object) {
    lua_pushnil(L);
    return;
  }

  lua_getfield(L, LUA_REGISTRYINDEX, "_lovrobjects");

  // Create the registry if it doesn't exist yet
  if (lua_isnil(L, -1)) {
    lua_newtable(L);
    lua_replace(L, -2);

    // Create the metatable
    lua_newtable(L);

    // __mode = v
    lua_pushliteral(L, "v");
    lua_setfield(L, -2, "__mode");

    // Set the metatable
    lua_setmetatable(L, -2);

    // Write the table to the registry
    lua_pushvalue(L, -1);
    lua_setfield(L, LUA_REGISTRYINDEX, "_lovrobjects");
  }

  lua_pushlightuserdata(L, object);
  lua_gettable(L, -2);

  if (lua_isnil(L, -1)) {
    lua_pop(L, 1);
  } else {
    lua_remove(L, -2);
    return;
  }

  // Allocate userdata
  Proxy* p = (Proxy*) lua_newuserdata(L, sizeof(Proxy));
  luaL_newmetatable(L, type);
  lua_setmetatable(L, -2);
  lovrRetain(object);
  p->object = object;
  p->hash = hash;

  // Write to registry and remove registry, leaving userdata on stack
  lua_pushlightuserdata(L, object);
  lua_pushvalue(L, -2);
  lua_settable(L, -4);
  lua_remove(L, -2);
}

int _luax_checkenum(lua_State* L, int index, const StringEntry* map, const char* fallback, const char* label) {
  size_t length;
  const char* string = fallback ? luaL_optlstring(L, index, fallback, &length) : luaL_checklstring(L, index, &length);

  for (int i = 0; map[i].length; i++) {
    if (map[i].length == length && !memcmp(map[i].string, string, length)) {
      return i;
    }
  }

  if (index > 0) {
    return luaL_argerror(L, index, lua_pushfstring(L, "invalid %s '%s'", label, string));
  } else {
    return luaL_error(L, "invalid %s '%s'", label, string);
  }

  return 0;
}

void luax_registerloader(lua_State* L, lua_CFunction loader, int index) {
  lua_getglobal(L, "table");
  lua_getfield(L, -1, "insert");
  lua_getglobal(L, "package");
#if LUA_VERSION_NUM == 501
  lua_getfield(L, -1, "loaders");
#else
  lua_getfield(L, -1, "searchers");
#endif
  lua_remove(L, -2);
  if (lua_istable(L, -1)) {
    lua_pushinteger(L, index);
    lua_pushcfunction(L, loader);
    lua_call(L, 3, 0);
  } else {
    lua_pop(L, 2);
  }
  lua_pop(L, 1);
}

int luax_resume(lua_State* T, int n) {
#if LUA_VERSION_NUM >= 504
  int results;
  return lua_resume(T, NULL, n, &results);
#elif LUA_VERSION_NUM >= 502 || defined(LOVR_USE_LUAU)
  return lua_resume(T, NULL, n);
#else
  return lua_resume(T, n);
#endif
}

int luax_loadbufferx(lua_State* L, const char* buffer, size_t size, const char* name, const char* mode) {
#ifdef LOVR_USE_LUAU
  size_t bytecodeSize = 0;
  char* bytecode = luau_compile(buffer, size, NULL, &bytecodeSize);
  int result = luau_load(L, name, bytecode, bytecodeSize, 0);
  free(bytecode);
  return result ? LUA_ERRSYNTAX : LUA_OK;
#elif LUA_VERSION_NUM >= 502
  return luaL_loadbufferx(L, buffer, size, name, mode);
#else
  bool binary = buffer[0] == LUA_SIGNATURE[0];
  if (mode && !strchr(mode, binary ? 'b' : 't')) {
    lua_pushliteral(L, "attempt to load chunk with wrong mode");
    return LUA_ERRSYNTAX;
  }
  return luaL_loadbuffer(L, buffer, size, name);
#endif
}

void luax_vthrow(void* context, const char* format, va_list args) {
  lua_State* L = (lua_State*) context;
  lua_pushvfstring(L, format, args);
  lua_error(L);
}

void luax_vlog(void* context, int level, const char* tag, const char* format, va_list args) {
  static const char* levels[] = {
    [LOG_DEBUG] = "debug",
    [LOG_INFO] = "info",
    [LOG_WARN] = "warn",
    [LOG_ERROR] = "error"
  };
  lua_State* L = (lua_State*) context;
  lua_getglobal(L, "lovr");
  lua_getfield(L, -1, "log");
  if (lua_type(L, -1) == LUA_TFUNCTION) {
    lua_pushvfstring(L, format, args);
    lua_pushstring(L, levels[level]);
    lua_pushstring(L, tag);
    lua_call(L, 3, 0);
  }
  lua_pop(L, 1);
}

// An implementation of luaL_traceback for Lua 5.1
void luax_traceback(lua_State* L, lua_State* T, const char* message, int level) {
  if (!lua_checkstack(L, 5)) {
    return;
  }

  lua_getglobal(L, "debug");
  if (!lua_istable(L, -1)) {
    lua_pop(L, 1);
    return;
  }

  lua_getfield(L, -1, "traceback");
  if (!lua_isfunction(L, -1)) {
    lua_pop(L, 2);
    return;
  }

  lua_remove(L, -2);
  lua_pushthread(T);
  lua_pushstring(L, message);
  lua_pushinteger(L, level);
  lua_call(L, 3, 1);
}

int luax_getstack(lua_State *L) {
  luax_traceback(L, L, lua_tostring(L, 1), 2);
  return 1;
}

int luax_pushsuccess(lua_State* L, bool success) {
  if (success) {
    lua_pushboolean(L, true);
    return 1;
  } else {
    lua_pushboolean(L, false);
    lua_pushstring(L, lovrGetError());
    return 2;
  }
}

void luax_pushconf(lua_State* L) {
  lua_getfield(L, LUA_REGISTRYINDEX, "_lovrconf");
}

int luax_setconf(lua_State* L) {
  luax_pushconf(L);
  luax_check(L, lua_isnil(L, -1), "Unable to set lovr.conf multiple times");
  lua_pop(L, 1);
  lua_setfield(L, LUA_REGISTRYINDEX, "_lovrconf");
  return 0;
}

void luax_pushstash(lua_State* L, const char* name) {
  lua_getfield(L, LUA_REGISTRYINDEX, name);

  if (lua_isnil(L, -1)) {
    lua_newtable(L);
    lua_replace(L, -2);

    // metatable
    lua_newtable(L);
    lua_pushliteral(L, "k");
    lua_setfield(L, -2, "__mode");
    lua_setmetatable(L, -2);

    lua_pushvalue(L, -1);
    lua_setfield(L, LUA_REGISTRYINDEX, name);
  }
}

void luax_setmainthread(lua_State *L) {
#if LUA_VERSION_NUM < 502
  lua_pushthread(L);
  lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
#endif
}

void luax_atexit(lua_State* L, voidFn* finalizer) {
  lua_getfield(L, LUA_REGISTRYINDEX, "_lovrfinalizers");

  if (lua_isnil(L, -1)) {
    lua_newtable(L);
    lua_replace(L, -2);

    // Userdata sentinel since tables don't have __gc (yet)
    lua_newuserdata(L, sizeof(void*));
    lua_createtable(L, 0, 1);
    lua_pushcfunction(L, luax_runfinalizers);
    lua_setfield(L, -2, "__gc");
    lua_setmetatable(L, -2);
    lua_setfield(L, -2, "");

    // Write to the registry
    lua_pushvalue(L, -1);
    lua_setfield(L, LUA_REGISTRYINDEX, "_lovrfinalizers");
  }

  int length = luax_len(L, -1);
  lua_pushcfunction(L, (lua_CFunction) finalizer);
  lua_rawseti(L, -2, length + 1);
  lua_pop(L, 1);
}

uint32_t _luax_checku32(lua_State* L, int index) {
  double x = lua_tonumber(L, index);

  if (x == 0. && !lua_isnumber(L, index)) {
    luax_typeerror(L, index, "number");
  }

  if (x < 0. || x > UINT32_MAX) {
    const char* message = lua_pushfstring(L, "expected a number between 0 and %u, got %g", UINT32_MAX, x);
    luaL_argerror(L, index, message);
  }

  return (uint32_t) x;
}

uint32_t _luax_optu32(lua_State* L, int index, uint32_t fallback) {
  return luaL_opt(L, luax_checku32, index, fallback);
}

void luax_readcolor(lua_State* L, int index, float color[4]) {
  color[0] = color[1] = color[2] = color[3] = 1.f;

  if (lua_istable(L, index)) {
    for (int i = 1; i <= 4; i++) {
      lua_rawgeti(L, index, i);
    }
    color[0] = luax_checkfloat(L, -4);
    color[1] = luax_checkfloat(L, -3);
    color[2] = luax_checkfloat(L, -2);
    color[3] = luax_optfloat(L, -1, 1.);
    lua_pop(L, 4);
#ifdef LOVR_USE_LUAU
  } else if (lua_isvector(L, index)) {
    vec3_init(color, lua_tovector(L, index));
    color[3] = 1.f;
#endif
  } else if (lua_gettop(L) >= index + 2) {
    color[0] = luax_checkfloat(L, index);
    color[1] = luax_checkfloat(L, index + 1);
    color[2] = luax_checkfloat(L, index + 2);
    color[3] = luax_optfloat(L, index + 3, 1.);
  } else if (lua_gettop(L) <= index + 1) {
    uint32_t x = luaL_checkinteger(L, index);
    color[0] = ((x >> 16) & 0xff) / 255.f;
    color[1] = ((x >> 8) & 0xff) / 255.f;
    color[2] = ((x >> 0) & 0xff) / 255.f;
    color[3] = luax_optfloat(L, index + 1, 1.);
  }
}

// Like readcolor, but only consumes 1 argument (nil, hex, table, vec3, vec4), useful for table keys
void luax_optcolor(lua_State* L, int index, float color[4]) {
  switch (lua_type(L, index)) {
    case LUA_TNONE:
    case LUA_TNIL:
      color[0] = color[1] = color[2] = color[3] = 1.f;
      break;
    case LUA_TNUMBER: {
      uint32_t x = lua_tonumber(L, index);
      color[0] = ((x >> 16) & 0xff) / 255.f;
      color[1] = ((x >> 8) & 0xff) / 255.f;
      color[2] = ((x >> 0) & 0xff) / 255.f;
      color[3] = 1.f;
      break;
    }
    case LUA_TTABLE:
      index = index > 0 ? index : (index + lua_gettop(L) + 1);
      for (int i = 1; i <= 4; i++) {
        lua_rawgeti(L, index, i);
      }
      color[0] = luax_checkfloat(L, -4);
      color[1] = luax_checkfloat(L, -3);
      color[2] = luax_checkfloat(L, -2);
      color[3] = luax_optfloat(L, -1, 1.);
      lua_pop(L, 4);
      break;
#ifdef LOVR_USE_LUAU
    case LUA_TVECTOR: {
      vec3_init(color, lua_tovector(L, index));
      color[3] = 1.f;
      break;
    }
#endif
    default: luaL_error(L, "Expected nil, number, table, vec3, or vec4 for color value");
  }
}

int luax_readmesh(lua_State* L, int index, float** vertices, uint32_t* vertexCount, uint32_t** indices, uint32_t* indexCount) {
  if (lua_istable(L, index)) {
    lua_rawgeti(L, index, 1);
    bool nested = lua_type(L, -1) == LUA_TTABLE;
    lua_pop(L, 1);

    *vertexCount = luax_len(L, index) / (nested ? 1 : 3);
    luax_check(L, *vertexCount > 0, "Invalid mesh data: vertex count is zero");
    *vertices = lovrMalloc(sizeof(float) * *vertexCount * 3);

    if (nested) {
      for (uint32_t i = 0; i < *vertexCount; i++) {
        lua_rawgeti(L, index, i + 1);
        lua_rawgeti(L, -1, 1);
        lua_rawgeti(L, -2, 2);
        lua_rawgeti(L, -3, 3);
        (*vertices)[3 * i + 0] = luax_checkfloat(L, -3);
        (*vertices)[3 * i + 1] = luax_checkfloat(L, -2);
        (*vertices)[3 * i + 2] = luax_checkfloat(L, -1);
        lua_pop(L, 4);
      }
    } else {
      for (uint32_t i = 0; i < *vertexCount * 3; i++) {
        lua_rawgeti(L, index, i + 1);
        (*vertices)[i] = luax_checkfloat(L, -1);
        lua_pop(L, 1);
      }
    }

    if (indices) {
      luaL_checktype(L, index + 1, LUA_TTABLE);
      *indexCount = luax_len(L, index + 1);
      luax_check(L, *indexCount > 0, "Invalid mesh data: index count is zero");
      luax_check(L, *indexCount % 3 == 0, "Index count must be a multiple of 3");
      *indices = lovrMalloc(sizeof(uint32_t) * *indexCount);

      for (uint32_t i = 0; i < *indexCount; i++) {
        lua_rawgeti(L, index + 1, i + 1);
        uint32_t index = luaL_checkinteger(L, -1) - 1;
        luax_check(L, index < *vertexCount, "Invalid vertex index %d (expected [%d, %d])", index + 1, 1, *vertexCount);
        (*indices)[i] = index;
        lua_pop(L, 1);
      }
    }

    return index + (indices ? 2 : 1);
  }

  ModelData* modelData = luax_totype(L, index, ModelData);

  if (modelData) {
    lovrModelDataGetTriangles(modelData, vertices, indices, vertexCount, indexCount);
    return index + 1;
  }

#ifndef LOVR_DISABLE_GRAPHICS
  Mesh* mesh = luax_totype(L, index, Mesh);

  if (mesh) {
    luax_assert(L, lovrMeshGetTriangles(mesh, vertices, indices, vertexCount, indexCount));
    return index + 1;
  }

  luaL_argerror(L, index, "table, ModelData, or Mesh expected");
#else
  luaL_argerror(L, index, "table or ModelData expected");
#endif
  return 0;
}

int luax_readvec3(lua_State* L, int index, vec3 v, const char* expected) {
  switch (lua_type(L, index)) {
    case LUA_TNIL:
    case LUA_TNONE:
      v[0] = v[1] = v[2] = 0.f;
      return index + 1;
    case LUA_TNUMBER:
      v[0] = luax_tofloat(L, index);
      v[1] = luax_optfloat(L, index + 1, v[0]);
      v[2] = luax_optfloat(L, index + 2, v[0]);
      return index + 3;
    case LUA_TTABLE:
      index = index > 0 ? index : (index + lua_gettop(L) + 1);
      if (luax_len(L, index) > 0) {
        lua_rawgeti(L, index, 1);
        lua_rawgeti(L, index, 2);
        lua_rawgeti(L, index, 3);
        v[0] = luax_tofloat(L, -3);
        v[1] = luax_tofloat(L, -2);
        v[2] = luax_tofloat(L, -1);
        lua_pop(L, 3);
      } else {
        lua_pushliteral(L, "x");
        lua_gettable(L, index);

        lua_pushliteral(L, "y");
        lua_gettable(L, index);

        lua_pushliteral(L, "z");
        lua_gettable(L, index);

        v[0] = luax_tofloat(L, -3);
        v[1] = luax_tofloat(L, -2);
        v[2] = luax_tofloat(L, -1);
        lua_pop(L, 3);
      }

      return index + 1;
#ifdef LOVR_USE_LUAU
    case LUA_TVECTOR:
      vec3_init(v, lua_tovector(L, index));
      return index + 1;
#endif
    default: return luax_typeerror(L, index, "number, table, or vector");
  }
}

int luax_readscale(lua_State* L, int index, vec3 v, int components, const char* expected) {
  switch (lua_type(L, index)) {
    case LUA_TNIL:
    case LUA_TNONE:
      v[0] = v[1] = v[2] = 1.f;
      return index + 1;
    case LUA_TNUMBER:
      if (components == 1) {
        v[0] = v[1] = v[2] = luax_tofloat(L, index);
      } else if (components == -2) { // -2 is special and means "2 components: xy and z"
        v[0] = v[1] = luax_tofloat(L, index);
        v[2] = luax_optfloat(L, index + 1, 1.f);
        return index + 2;
      } else {
        v[0] = v[1] = v[2] = 1.f;
        for (int i = 0; i < components; i++) {
          v[i] = luax_optfloat(L, index + i, v[0]);
        }
      }
      return index + components;
    case LUA_TTABLE:
      index = index > 0 ? index : (index + lua_gettop(L) + 1);
      if (luax_len(L, index) > 0) {
        lua_rawgeti(L, index, 1);
        lua_rawgeti(L, index, 2);
        lua_rawgeti(L, index, 3);
        v[0] = luax_tofloat(L, -3);
        v[1] = luax_tofloat(L, -2);
        v[2] = luax_tofloat(L, -1);
        lua_pop(L, 3);
      } else {
        lua_pushliteral(L, "x");
        lua_gettable(L, index);

        lua_pushliteral(L, "y");
        lua_gettable(L, index);

        lua_pushliteral(L, "z");
        lua_gettable(L, index);

        v[0] = luax_tofloat(L, -3);
        v[1] = luax_tofloat(L, -2);
        v[2] = luax_tofloat(L, -1);
        lua_pop(L, 3);
      }
      return index + 1;
#ifdef LOVR_USE_LUAU
    case LUA_TVECTOR:
      vec3_init(v, lua_tovector(L, index));
      return index + 1;
#endif
    default: return luax_typeerror(L, index, expected ? expected : "nil, number, table, or vector");
  }
}

int luax_readquat(lua_State* L, int index, quat q, const char* expected) {
  float angle, ax, ay, az;
  switch (lua_type(L, index)) {
    case LUA_TNIL:
    case LUA_TNONE:
      quat_identity(q);
      return index + 1;
    case LUA_TNUMBER:
      angle = luax_optfloat(L, index, 0.f);
      ax = luax_optfloat(L, index + 1, 0.f);
      ay = luax_optfloat(L, index + 2, 1.f);
      az = luax_optfloat(L, index + 3, 0.f);
      quat_fromAngleAxis(q, angle, ax, ay, az);
      return index + 4;
    case LUA_TTABLE:
      index = index > 0 ? index : (index + lua_gettop(L) + 1);
      if (luax_len(L, index) > 0) {
        lua_rawgeti(L, index, 1);
        lua_rawgeti(L, index, 2);
        lua_rawgeti(L, index, 3);
        lua_rawgeti(L, index, 4);
        q[0] = luax_tofloat(L, -4);
        q[1] = luax_tofloat(L, -3);
        q[2] = luax_tofloat(L, -2);
        q[3] = luax_tofloat(L, -1);
        lua_pop(L, 4);
      } else {
        lua_pushliteral(L, "x");
        lua_gettable(L, index);

        lua_pushliteral(L, "y");
        lua_gettable(L, index);

        lua_pushliteral(L, "z");
        lua_gettable(L, index);

        lua_pushliteral(L, "w");
        lua_gettable(L, index);

        q[0] = luax_tofloat(L, -4);
        q[1] = luax_tofloat(L, -3);
        q[2] = luax_tofloat(L, -2);
        q[3] = luax_tofloat(L, -1);
        lua_pop(L, 4);
      }
      return index + 1;
#ifdef LOVR_USE_LUAU
    case LUA_TQUATERNION: {
      const short* s = lua_toquaternion(L, index);
      q[0] = MAX(s[0] / 32767.f, -1.f);
      q[1] = MAX(s[1] / 32767.f, -1.f);
      q[2] = MAX(s[2] / 32767.f, -1.f);
      q[3] = MAX(s[3] / 32767.f, -1.f);
      quat_normalize(q);
      return index + 1;
    }
#endif
    default: return luax_typeerror(L, index, expected ? expected : "nil, number, table, or quaternion");
  }
}

int luax_readmat4(lua_State* L, int index, mat4 m, int scaleComponents) {
  switch (lua_type(L, index)) {
    case LUA_TNIL:
    case LUA_TNONE:
      mat4_identity(m);
      return index + 1;
#ifdef LOVR_USE_LUAU
    case LUA_TVECTOR:
#endif
    case LUA_TNUMBER:
    case LUA_TTABLE:;
      float T[3], R[4], S[3];
      index = luax_readvec3(L, index, T, "number, table, vector, or Mat4");
      index = luax_readscale(L, index, S, scaleComponents, NULL);
      index = luax_readquat(L, index, R, NULL);
      mat4_fromPose(m, T, R);
      mat4_scale(m, S[0], S[1], S[2]);
      return index;
    default:;
      Mat4* matrix = luax_totype(L, index, Mat4);
      if (matrix) {
        mat4_init(m, lovrMat4GetData(matrix));
        return index + 1;
      }
      return luax_typeerror(L, index, "number, table, vector, or Mat4");
  }
}

void luax_pushvec3(lua_State* L, float v[3], bool tableArray) {
  if (tableArray) {
    lua_createtable(L, 3, 0);
    lua_pushnumber(L, v[0]);
    lua_rawseti(L, -2, 1);
    lua_pushnumber(L, v[1]);
    lua_rawseti(L, -2, 2);
    lua_pushnumber(L, v[2]);
    lua_rawseti(L, -2, 3);
  } else {
    lua_createtable(L, 0, 3);
    lua_pushnumber(L, v[0]);
    lua_setfield(L, -2, "x");
    lua_pushnumber(L, v[1]);
    lua_setfield(L, -2, "y");
    lua_pushnumber(L, v[2]);
    lua_setfield(L, -2, "z");
  }
}

bool luax_isquat(lua_State* L, int index) {
  if (lua_istable(L, index)) {
    int len = luax_len(L, index);
    if (len == 4) {
      return true;
    } else if (len == 0) {
      lua_pushstring(L, "w");
      lua_gettable(L, index);
      bool number = lua_type(L, -1) == LUA_TNUMBER;
      lua_pop(L, 1);
      return number;
    }
  }

#ifdef LOVR_USE_LUAU
  return lua_type(L, index) == LUA_TQUATERNION;
#endif

  return false;
}
